﻿namespace FODATAC.LinqProvider

open System
open System.Linq
open System.Linq.Expressions
open System.Collections.Generic
open System.Collections
open System.Text.RegularExpressions
open System.Text
open FODATAC.Interfaces.Internal
open System.Threading.Tasks
open System.Reflection
open FODATAC.Extensions
open FODATAC.CommonTypes

type LinqExpressionVisitor() = 
    inherit ExpressionVisitor()
    let mutable isCount = false
    let mutable filter = ""
    let mutable orderBy = ""
    let mutable skip = ""
    let mutable first = ""
    let mutable take = ""
    let mutable specialFunction = ""
    let expandList = List<string>()
    let selectList = List<string>()
    let andExpressionStack = Stack<string>()
    let orExpressionStack = Stack<string>()
    let visitedMethods = HashSet()
    
    member this.Parse(expression : Expression) = 
        this.Visit(expression) |> ignore
        let mutable result = ""
        if (isCount) then result <- result + "/$count"
        if not (String.IsNullOrWhiteSpace filter) then result <- result.AddParameter("$filter=" + filter)
        this.ProcessAdditionalTokens result
    
    member this.ProcessAdditionalTokens req = 
        let mutable res = req
        
        let addParam (str, res : string) = 
            if not (String.IsNullOrWhiteSpace str) then res.AddParameter(str)
            else res
        res <- addParam (orderBy, res)
        res <- addParam (first, res)
        res <- addParam (skip, res)
        res <- addParam (take, res)
        let handleList (list : List<string>, str, res : string) = 
            let items = String.Join(",", list)
            if not (list|>Seq.isEmpty) then res.AddParameter(str + items)
            else res
        res <- handleList (expandList, "$expand=", res)
        res <- handleList (selectList, "$select=", res)
        res
    
    override this.VisitMethodCall(m : MethodCallExpression) = 
        match m.Method.Name with
        | "ToUpper" | "Length" | "YearCompare" | "DayCompare" -> ()
        | "FirstOrDefault" | "First" -> this.SetFirst(m)
        | "OrderBy" -> this.SetOrderingClause(m)
        | "OrderByDescending" -> this.SetDescOrderingClause(m)
        | "Count" -> isCount <- true
        | "GroupBy" | "StartsWith" | "Contains" | "Where" | "EndsWith" -> this.SetWhereQuery(m)
        | "Skip" -> this.SetSkip(m)
        | "Take" -> this.SetTake(m)
        | "Fetch" -> this.SetFetch(m)
        | "Select" -> this.SetSelect(m)
        | "AsQueryable" -> ()
        | _ -> failwith "unsupprted method %s" m.Method.Name
        if visitedMethods.Contains m then m :> Expression
        else base.VisitMethodCall(m)
    
    member this.SetWhereQuery(m : MethodCallExpression) = 
        if m.Arguments.Count <> 1 then 
            let arg = m.Arguments.[1]
            let unary = arg :?> UnaryExpression
            let op = unary.Operand :?> LambdaExpression
            op.Body
            |> this.CreateOperand
            |> this.AppendFilter
    
    member private this.GetMemberChain(mex : MemberExpression) = 
        let members = List()
        
        let rec loop (mex : MemberExpression) = 
            members.Add(mex.Member.Name)
            let expression = mex.Expression
            match expression with
            | :? MemberExpression as me -> loop (me)
            | _ -> ()
        loop (mex)
        members.Reverse()
        String.Join("/", members)
    
    member private this.GetMemberChain(m : MethodCallExpression):option<string> = 
        if visitedMethods.Contains m then None
        else
            if m.Arguments.Count <> 1 then 
            
                let rec extractFromUnary (unary:UnaryExpression):option<string> =
                    let lambda = unary.Operand :?> LambdaExpression
                    match lambda.Body with
                    | :? MemberExpression as me -> this.GetMemberChain me |> Some
                    | :? MethodCallExpression as mc when mc.Arguments.Count = 2 ->
                        if visitedMethods.Add mc then
                            match mc.Arguments.[0] with
                            | :? MethodCallExpression as  mc2 when  mc2.Method.Name = "AsQueryable" ->
                                visitedMethods.Add mc2 |> ignore
                                let left =  (mc2.Arguments.[0] :?> MemberExpression) |> this.GetMemberChain
                                let memNames = ( mc.Arguments.[1] :?> UnaryExpression) |> extractFromUnary
                                match memNames with 
                                    | Some memNames ->
                                        (sprintf "%s/%s" left memNames) |> Some
                                    | None ->None
                            |  other -> failwithf "unsupporte expression %A" other
                        else None
                    | other -> failwithf "unsupporte expression %A" other
                
                let arg = m.Arguments.[1] :?> UnaryExpression
                let res = extractFromUnary arg      
                res
            
            else failwith "Cannot calculate member chain since ther are more than one arguments in MethodCallExpression %A" m
    
    member this.SetFetch(m : MethodCallExpression) = match this.GetMemberChain(m) with | Some x ->x|> expandList.Add | None->()
    
    member this.SetSelect(m : MethodCallExpression) = 
        let res = this.GetMemberChain(m)
        match res with 
        | Some res ->
            res |> selectList.Add
            res |> expandList.Add
        |_ -> ()
    
    member this.AppendFilter param = 
        if (String.IsNullOrWhiteSpace filter) then filter <- param
        else raise (Exception("Unable to Create Operand"))
    
    member this.CreateOperand(expression : Expression) = 
        match expression with
        | :? BinaryExpression as binary -> 
            if this.IsSpecialFunction(binary.Left.ToString()) then this.TranslateSpecialFunction(binary)
            else this.CreateODataOperand binary
        | :? MethodCallExpression as methodCall -> this.CreateOperand methodCall
        | _ -> raise (Exception("Unable to create operand"))
    
    member this.CreateOperand(methodCall : MethodCallExpression) = 
        match (methodCall.Method.Name) with
        | "StartsWith" -> this.CreateUnary("startsWith", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | "EndsWith" -> this.CreateUnary("endsWith", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | "ToUpper" -> this.CreateUnary("toupper", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | "ToLower" -> this.CreateUnary("tolower", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | "Trim" -> this.CreateUnary("trim", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | "Contains" -> this.CreateUnary("substringof", methodCall.Object :?> MemberExpression, methodCall.Arguments.[0])
        | _ -> raise (ArgumentOutOfRangeException())
    
    member this.CreateODataOperand(body : BinaryExpression) = 
        match body.NodeType with
        | ExpressionType.GreaterThan | ExpressionType.GreaterThanOrEqual | ExpressionType.LessThan | ExpressionType.LessThanOrEqual | ExpressionType.Equal | ExpressionType.NotEqual -> 
            this.CreateUnary(body.NodeType, body.Left :?> MemberExpression, body.Right)
        | ExpressionType.And | ExpressionType.AndAlso -> this.CreateAnd(body.Left, body.Right)
        | ExpressionType.Or | ExpressionType.OrElse -> this.CreateOr(body.Left, body.Right)
        | _ -> failwith "unsupported expression type"
    
    member this.CreateAnd(left : Expression, right : Expression) = 
        let sb = StringBuilder()
        
        let rec recurse (lexp : Expression) = 
            match lexp.NodeType with
            | ExpressionType.And | ExpressionType.AndAlso -> 
                let exp = lexp :?> BinaryExpression
                exp.Right
                |> this.CreateOperand
                |> andExpressionStack.Push
            | _ -> 
                lexp
                |> this.CreateOperand
                |> andExpressionStack.Push
        match left.NodeType with
        | ExpressionType.And | ExpressionType.AndAlso -> recurse (left)
        | _ -> 
            left
            |> this.CreateOperand
            |> andExpressionStack.Push
        let rightExpression = right |> this.CreateOperand
        for ex in andExpressionStack do
            sb.AppendFormat("{0} and ", ex) |> ignore
        String.Concat(sb.ToString(), rightExpression)
    
    member this.CreateOr(left : Expression, right : Expression) = 
        let sb = StringBuilder()
        
        let rec recurse (lexp : Expression) = 
            match lexp.NodeType with
            | ExpressionType.Or | ExpressionType.OrElse -> 
                let exp = lexp :?> BinaryExpression
                exp.Right
                |> this.CreateOperand
                |> orExpressionStack.Push
            | _ -> 
                lexp
                |> this.CreateOperand
                |> orExpressionStack.Push
        match left.NodeType with
        | ExpressionType.Or | ExpressionType.OrElse -> recurse (left)
        | _ -> 
            left
            |> this.CreateOperand
            |> orExpressionStack.Push
        let rightExpression = right |> this.CreateOperand
        for ex in orExpressionStack do
            sb.AppendFormat("{0} or ", ex) |> ignore
        String.Concat(sb.ToString(), rightExpression)
    
    member this.IsSpecialFunction leftExpression = 
        let r = Regex("""(\w+\.\w+\.Length)|(\w+\.\w+\.ToUpper)|(\w+\.\w+\.ToLower)|(\w+\.\w+\.YearCompare)""")
        let m = r.Match leftExpression
        if m.Success then 
            let matchh : System.String = m.Value
            specialFunction <- matchh.Substring(matchh.LastIndexOf(".") + 1)
            true
        else false
    
    member this.TranslateSpecialFunction(exp : BinaryExpression) = 
        match specialFunction.ToUpper() with
        | "LENGTH" -> this.TranslateLengthFunction exp
        | "TOLOWER" -> this.TranslateCaseFunction(exp, "tolower")
        | "TOUPPER" -> this.TranslateCaseFunction(exp, "toupper")
        | _ -> raise (ArgumentOutOfRangeException())
    
    member this.TranslateCaseFunction(exp : BinaryExpression, func : string) = 
        let lft = exp.Left :?> MethodCallExpression
        let left = lft.Object :?> MemberExpression
        let right = exp.Right :?> ConstantExpression
        System.String.Format("{2}({0}) eq {1}", (left.Member.Name) :> obj, (right.Value), func :> obj)
    
    member this.TranslateLengthFunction(exp : BinaryExpression) : string = 
        let nodeType = exp.NodeType
        let lft = exp.Left :?> MemberExpression
        let left = lft.Expression :?> MemberExpression
        let right = exp.Right
        let prop = this.ParseProperty(left.Member.Name)
        let prepareString op = System.String.Format("length({0}) " + op + " {1}", prop :> obj, this.GetValue(right))
        match nodeType with
        | ExpressionType.Equal -> prepareString "eq"
        | ExpressionType.NotEqual -> prepareString "ne"
        | ExpressionType.GreaterThan -> prepareString "gt"
        | ExpressionType.GreaterThanOrEqual -> prepareString "ge"
        | ExpressionType.LessThan -> prepareString "lt"
        | ExpressionType.LessThanOrEqual -> prepareString "le"
        | _ -> failwith "no match"
    
    member this.GetValue(memberExpression : Expression) : obj = 
        let res = Expression.Lambda(memberExpression).Compile().DynamicInvoke()
        if res = null then "null" :> obj
        else if memberExpression.Type = typeof<String> then ("'" + (res :?> String) + "'") :> obj
        else if memberExpression.Type = typeof<Int64> then ((res.ToString()) + "L") :> obj
        else if memberExpression.Type = typeof<Guid> then ( "guid'" + (res.ToString()) + "'") :> obj
        else res
    
    member this.ParseProperty(value : string) : string = value.Replace(".", "/")
    
    member this.SetFirst(m : MethodCallExpression) = 
        if m.Arguments.Count <> 1 then 
            let arg = m.Arguments.[1]
            let unary = arg :?> UnaryExpression
            let op = unary.Operand :?> LambdaExpression
            op.Body
            |> this.CreateOperand
            |> this.AppendFilter
            first <- "$top=1"
    
    member this.SetOrderingClause(m : MethodCallExpression, ?desc) = 
        let str = 
            match desc with
            | None -> "$orderby={0}"
            | Some(_) -> "$orderby={0} desc"
        if m.Arguments.Count <> 1 then 
            let arg = m.Arguments.[1]
            let unary = arg :?> UnaryExpression
            let op = unary.Operand :?> LambdaExpression
            let exp = op.Body :?> MemberExpression
            op.Body
            |> this.CreateOperand
            |> this.AppendFilter
            orderBy <- String.Format(str, (this.ParseProperty(exp.Member.Name)))
    
    member this.SetDescOrderingClause(m : MethodCallExpression, ?desc) = this.SetOrderingClause(m, Some(true))
    
    member this.SetTake(m : MethodCallExpression) = 
        if m.Arguments.Count <> 1 then 
            let arg = m.Arguments.[1] :?> ConstantExpression
            take <- String.Format("$top={0}", arg.Value)
    
    member this.SetSkip(m : MethodCallExpression) = 
        if m.Arguments.Count <> 1 then 
            let arg = m.Arguments.[1] :?> ConstantExpression
            take <- String.Format("skip={0}", arg.Value)
    
    member this.CreateUnary(name : string, left : MemberExpression, right : Expression) = 
        String.Format("{0}({1},'{2}')", name :> obj, this.ParseProperty(left.Member.Name) :> obj, this.GetValue(right))
    member this.CreateUnary(name : string, left : ConstantExpression, right : MemberExpression) = 
        String.Format("{0}('{1}',{2})", name, left.Value, this.ParseProperty(right.Member.Name))
    member this.CreateUnary(name : string, left : MemberExpression) = String.Format("{0}({1})", name, this.ParseProperty(left.Member.Name))
    member this.CreateUnary(nodeType : ExpressionType, left : MemberExpression, right : Expression) = 
        let prop = this.GetMemberChain left
        //let prop = this.ParseProperty(left.Member.Name)
        let prepareString op = String.Format("{0} " + op + " {1}", prop, this.GetValue(right))
        match nodeType with
        | ExpressionType.Equal -> prepareString "eq"
        | ExpressionType.NotEqual -> prepareString "ne"
        | ExpressionType.GreaterThan -> prepareString "gt"
        | ExpressionType.GreaterThanOrEqual -> prepareString "ge"
        | ExpressionType.LessThanOrEqual -> prepareString "le"
        | ExpressionType.LessThan -> prepareString "lt"
        | _ -> failwith "Unsupported operator"

[<AbstractClass>]
type public QueryProvider() = 
    
    interface IQueryProvider with
        
        member this.CreateQuery(expression : Expression) : IQueryable = 
            let elementType = expression.Type
            let ``type`` = typeof<LinqQuery<_>>.MakeGenericType(elementType)
            Activator.CreateInstance(``type``, 
                                     [| this :> obj
                                        expression :> obj |]) :?> IQueryable
        
        member this.CreateQuery<'T>(expression : Expression) : IQueryable<'T> = upcast LinqQuery(this, expression)
        member this.Execute<'TResult>(expression : Expression) : 'TResult = this.Execute<'TResult>(expression)
        member this.Execute(expression : Expression) : obj = this.Execute<Object>(expression)
    
    abstract Execute<'TResult> : Expression -> 'TResult
    abstract ExecuteAsync<'TResult> : Expression -> Async<IList<'TResult>>

and private LinqQuery<'T>(queryProvider : QueryProvider, collection : string, optExpression : option<Expression>) as this = 
    [<DefaultValue>] val mutable expression : Expression
    let fetchList = List()
    
    do 
        match optExpression with
        | Some(exp) -> this.expression <- exp
        | _ -> this.expression <- Expression.Constant(this)
        if not (typeof<IQueryable<'T>>.GetTypeInfo().IsAssignableFrom(this.expression.Type.GetTypeInfo())) then raise (ArgumentException("expression"))
    
    new(queryProvider, expression : Expression) = new LinqQuery<'T>(queryProvider, null, Some(expression))
    
    interface ILinqQuery with
        member this.CollectionName = collection
    
    interface FODATAC.Interfaces.IODataQueryable<'T> with
        member this.Expression = this.expression
        member this.ElementType = typeof<'T>
        member this.Provider : IQueryProvider = upcast queryProvider
        member this.GetEnumerator() : IEnumerator = upcast this.GetEnumerator()
        member this.GetEnumerator() : IEnumerator<'T> = this.GetEnumerator()
        member this.ToListAsync() : Task<IList<'T>> = this.ToListAsync()
        
        member this.FirstAsync() : Task<'T> = 
            let res = async { let! execute = queryProvider.ExecuteAsync<'T>(this.expression)
                              return execute.First() }
            convertToTask<'T> (res)
        
        member this.FirstOrDefaultAsync() : Task<'T> = 
            let res = async { let! execute = queryProvider.ExecuteAsync<'T>(this.expression)
                              return execute.FirstOrDefault() }
            convertToTask<'T> (res)
    
    member this.GetEnumerator() : IEnumerator<'T> = 
        let execute = queryProvider.Execute<IEnumerable<'T>>(this.expression)
        execute.GetEnumerator()
    
    member this.ToListAsync() : Task<IList<'T>> = 
        let res = async { let! execute = queryProvider.ExecuteAsync<'T>(this.expression)
                          return execute }
        convertToTask<IList<'T>> (res)
    
    new(queryProvider, collection : string) = new LinqQuery<'T>(queryProvider, collection, None)

type internal LinqProvider(session : ISessionInternal, baseUrl : string) = 
    inherit QueryProvider()
    override this.Execute<'TResult>(expression : Expression) : 'TResult = raise ((NotSupportedException()))
    
    override this.ExecuteAsync<'T>(expression : Expression) : Async<IList<'T>> = 
        let collectionName = expression.GetCollectionName()
        let requestUrl = sprintf "%s%s%s" baseUrl collectionName (this.Translate(expression))
        async { return! session.CreateQuery(requestUrl).ToListAsync<'T>() |> Async.AwaitTask }
    
    member this.CreateQuery<'T>(collection : string) : IQueryable<'T> = upcast LinqQuery<'T>(this, collection)
    member private this.Translate(expression : Expression) : string = 
        let visitor = LinqExpressionVisitor()
        visitor.Parse(expression)

type public OtherLinqProvider<'K>(baseUrl : string, action : Func<string,Task>) = 
        inherit QueryProvider()
        override this.Execute<'TResult>(expression : Expression) : 'TResult = raise ((NotSupportedException("only asynchronouse operations are supported")))
    
        override this.ExecuteAsync<'T>(expression : Expression) : Async<IList<'T>> = 
            let collectionName = expression.GetCollectionName()
            let requestUrl = sprintf "%s%s%s" baseUrl collectionName (this.Translate(expression))          
            (action.Invoke(requestUrl) :?> Task<IList<'T>> |> Async.AwaitTask)
    
        member this.CreateQuery<'T>(collection : string) : IQueryable<'T> = upcast LinqQuery<'T>(this, collection)
        member private this.Translate(expression : Expression) : string = 
            let visitor = LinqExpressionVisitor()
            visitor.Parse(expression)
